JavaScript 中的抽象类

在 JavaScript 中,抽象类的概念并不像 Java、TypeScript 和 Python 等其他语言那样得到原生支持。然而,我们可以通过编写自定义代码来模拟 JavaScript 中抽象类的行为。

本文将解释什么是抽象类、抽象类与函数式编程的对比,以及如何在 JavaScript 中实现抽象类。鉴于 TypeScript 是一种基于 JavaScript 的编程语言,这里将用它来解释抽象类的概念,以便于理解。

什么是抽象类?

在面向对象编程(OOP)中,所有的对象都是通过类来描述的。然而,并不是所有的类都用于描述具体的对象。如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类

抽象类除了不能实例化对象之外,类的其他功能依然存在。成员变量、成员方法和构造方法的访问方式与普通类一样。

由于抽象类不能实例化对象,因此抽象类必须被继承才能使用。这也是为什么通常在设计阶段就要决定是否要设计抽象类。

父类包含了子类集合的常见方法,但由于父类本身是抽象的,因此不能直接使用这些方法。

示例:TypeScript 中的抽象类

在 TypeScript 中,类、方法和字段可以是抽象的。抽象方法或抽象字段是尚未提供实现的方法或字段。这些成员必须存在于抽象类中,而抽象类不能直接实例化。

抽象类的作用是作为子类的基类,子类会实现所有抽象成员。如果一个类没有任何抽象成员,则称其为具体类

以下是一个抽象类 BaseConfigUtils 的示例:

export abstract class BaseConfigUtils<
  T extends BaseConfig,
  InitOptions extends BaseOptions,
  ResolvedOptions extends InitOptions
> {
  // ...existing code...

  constructor(options: ConfigOptions<T, InitOptions, ResolvedOptions>) {
    // ...existing code...
  }

  async resolveConfig(opts: unknown): Promise<T> {
    // ...existing code...
  }

  protected async handleNonInteractiveMode(
    opts: InitOptions,
    existingConfig: T | null
  ): Promise<T> {
    // ...existing code...
  }

  protected abstract handleInteractiveMode(
    opts: InitOptions,
    existingConfig: T | null
  ): Promise<T>;

  protected abstract mergeConfig(
    opts: InitOptions,
    existingConfig: T
  ): Promise<T>;

  protected abstract transformToConfig(opts: ResolvedOptions): T;

  protected abstract getConfigIdentifier(opts: InitOptions): string;

  // ...existing code...
}

在这个示例中,BaseConfigUtils 定义了处理配置文件的结构。子类必须实现 handleInteractiveModemergeConfigtransformToConfiggetConfigIdentifier 等方法。

函数式编程方法

函数式编程(FP)是一种将计算视为数学函数求值的范式,它避免改变状态和可变数据。FP 不使用类和继承,而是依赖纯函数和高阶函数。

示例:TypeScript中的函数式编程

以下是使用函数式编程实现类似功能的示例:

type ConfigKey = 'client' | 'mocks';

interface BaseConfig {
  [key: string]: string;
}

interface BaseOptions {
  yes: boolean;
  cwd: string;
  [key: string]: unknown;
}

interface ConfigOptions<T extends BaseConfig, InitOptions extends BaseOptions, ResolvedOptions extends InitOptions> {
  configKey: ConfigKey;
  initOptionsSchema: z.ZodSchema<InitOptions>;
  resolvedOptionsSchema: z.ZodSchema<ResolvedOptions>;
  defaultConfig: T;
  command: Command;
  cwd: string;
  handleInteractiveMode(
    opts: InitOptions,
    existingConfig: T | null
  ): Promise<T>;
  transformToConfig(opts: ResolvedOptions): T;
  getConfigIdentifier(opts: InitOptions): string;
  mergeConfig(
    opts: InitOptions,
    existingConfig: T
  ): Promise<T>;
}

const resolveConfig = async <T extends BaseConfig, InitOptions extends BaseOptions, ResolvedOptions extends InitOptions>(
  options: ConfigOptions<T, InitOptions, ResolvedOptions>,
  opts: unknown
): Promise<T> => {
  const { transformToConfig, getConfigIdentifier, mergeConfig, handleInteractiveMode } = options;
  const validatedOpts = await options.initOptionsSchema.parseAsync(opts).catch((error) => {
    handleSchemaError(error, options.command);
  });

  const existingConfig = await getRawConfigs(options.cwd, options.configKey, getConfigIdentifier(validatedOpts));

  if (validatedOpts.yes) {
    return handleNonInteractiveMode(options, validatedOpts, existingConfig);
  }

  return handleInteractiveMode(options, validatedOpts, existingConfig);
};

const handleNonInteractiveMode = async <T extends BaseConfig, InitOptions extends BaseOptions, ResolvedOptions extends InitOptions>(
  options: ConfigOptions<T, InitOptions, ResolvedOptions>,
  opts: InitOptions,
  existingConfig: T | null
): Promise<T> => {
  if (existingConfig) {
    return mergeConfig(opts, existingConfig);
  }

  try {
    const validatedOpts = await options.resolvedOptionsSchema.parseAsync(opts);
    return transformToConfig(validatedOpts);
  } catch (error) {
    handleSchemaError(error, options.command);
  }
};

// 定义其他函数,如 getRawConfigs

抽象类与函数式编程的比较

抽象类

  • 优点
    • 结构清晰,组织有序。
    • 强制一致的接口。
    • 对于熟悉面向对象编程(OOP)的开发人员来说更容易理解。
  • 缺点
    • 可能导致复杂的继承层次结构。
    • 在组合方面灵活性较差。

函数式编程

  • 优点
    • 提倡不变性和纯函数。
    • 更容易组合和重用函数。
    • 避免了继承的陷阱。
  • 缺点
    • 对于习惯于 OOP 的开发人员来说可能更难理解。
    • 在管理状态和依赖项时可能会导致更多的样板代码。

JavaScript 中的抽象类

class Base {
  constructor(name) {
    if (this.constructor == Base) {
      throw new Error("Class is of abstract type and can't be instantiated");
    }

    if (this.getName == undefined) {
      throw new Error('getName method must be implemented');
    }
    this.name = name;
  }

  printName() {
    console.log('Hello, ' + this.getName());
  }
}

class Derived extends Base {
  getName() {
     return 'world';
  }
}

// const b = new Base();
const d = new Derived();

d.printName();

结论

抽象类和函数式编程各有优缺点。抽象类提供了一种清晰且结构化的方法来强制接口和共享行为,而函数式编程则提供了灵活性并提倡不变性。选择哪种方法取决于项目的具体需求和团队对每种范式的熟悉程度。

如果想要在 JavaScript 中创建抽象类,建议使用 TypeScript,因为它不仅提供了类型安全性,还原生支持抽象类的概念。